Redis API & Java RedisTemplate深入分析

Redis API

Redis是一种基于键值对的NoSQL数据库。

在展开Redis API之前作为开发者的我们无论在用什么样的编程语言,开发什么样的项目都会有使用到将数据缓存在内存中的场景。

如果让我们自己开设计并开发一款基于键值对的缓存数据库我们该如何实现?

支持哪些数据结构?

  • 作为java coder的笔者就经常遇到需要将配置信息、热点高频数据、统计数据、高性能需求数据缓存到String、List、Map等数据结构的需求。

在缓存数据时需要根据需求选择合适的数据结构,Redis中提供了5种基本的数据结构。

  • string
  • hash
  • list
  • set
  • zset

string

字符串是Redis中最基本的数据结构。Redis中的健都是以字符串进行存储的。字符串可以是简单字符串、复杂字符串(JSON、XML)、数字(整形、浮点型)、二进制(图片、音频、视屏),其最大值不能超过512MB。

字符串的使用场景很多可以将对象转换成json字符串存储在Redis中。在分布式web服务器中也可以使用字符串存储用户session,保证请求在路由到新机器时能够识别用户身份无需二次登录。

当字符串存储数字时可以实现高性能分布式线程安全的的快速计数(类似Java中的AtomicLong#incrementAndGet 需要使用CAS实现线程安全,而Redis天生的单线程模型使其简单高效地实现了计数功能),实现很多统计功能。

hash

field value
id 1
name 小明
age 19
birthday 1999-09-09

哈希类型可以看做java中的map类型。在Redis中的哈希类型的映射关系为field-value不是健对应的值,需要注意value不同的上下文。

哈希可以存储关系型数据库表中的字段和值,如可以将用户信息存储在hash

list

列表类型用于存储多个有序的字符串。在Redis中可以对列表的两端进行插入和弹出(类似java中的Deque双端列队),也可以像数组一样通过下标获取对应的值。双向链表的结构使其既可以充当栈也可以充当列队的角色。

可以使用其从左边push右边pop的特性实现消息列队,比如注册成功后的邮件通知使用Redis消息列队相比于MQ中间件将更加轻量易于维护。

使用列表的有序性以及可以按下标和范围查找的特性缓存数据库中的需要分页显示的列表数据。

微信朋友圈的动态就可以使用list进行实现,每当有好友发布动态时就向list中存储动态的id。其有序性保证了时间轴的实现。

set

集合用来保存多个不同的字符串元素。和java中的set一样,集合是无序的不能用下标进行访问,集合的唯一性可以用来存储标签系统中的tag如用户的兴趣爱好或是新闻系统中用户关注的栏目等。

Redis中的集合类型NB的地方在于除了基本的增删改查操作外还支持集合间的交集、并集、差集运算,这种特性将非常方便地解决了社交网络应用中的很多需求,如共同关注、共同喜好、二度好友等功能。

此外Redis提供随机获取集合中元素的api可以用于生成随机数的业务中如抽奖系统等。

zset

有序集合是在集合的基础上为每个元素设置分数(score)作为排序依据。有序集合增加了获取指定分数的元素和元素范围查找、计算成员排名功能。

有序集合可以用在社交和游戏中的排行榜需求中。

如何存储数据(内部编码)?

确定了支持的数据结构后我们需要设计合理的编码(存储的大小和查询的时间复杂度)方式将不同数据结构的数据编码成二进制数据存储在内存中。

在Redis中不同的数据结构都有多种内部编码方式,在使用时需要根据实际的情况选择合适的编码以达到时间和空间的平衡。

内部编码.png

如何操作数据(命令、协议)?

我们还需要设计对外开放的api 供外部系统访问数据。

redis-cli

Redis提供了redis-cli 可以在命令行操作数据
Redis是一种基于键值对的NoSQL数据库,它的5种数据结构都是健值对中的值。对于健来说有一些通用命令。

命令 描述
keys * 查看所有健
dbsize 健总数
exists key 检查健是否存在
del key [key ...] 删除键
expire key seconds 键过期
type key 键的数据结构类型
object encoding key 值的内部编码

后端开发的同学们在接触Redis之前肯定学过至少一种关系型数据库,下面以关系型数据库的增、删、改、查来总结Redis中不同数据结构的操作命令

string set key value [ex seconds] [px milliseconds] [nx|xx] ex seconds:为健设置秒级过期时间 px milliseconds:为健设置毫秒级过期时间 nx : 健必须不存在才能成功,用于新增 xx:健必须存在才能成功,用于更新 del key 同增 get key
hash hset key field value hdel key field 同增 hget key field
list rpush key value [value ...] lpush key value [value ...] linsert key before|after piovt value lpop key rpop key lrem count value ltrim key start end lset key index value lrange key start end lindex key index lllen key
set sadd key element [element ...] srem key element [element ...] 同增 scard key sismember key element srandmember key [count] spop key smenbers key
zset zadd key score member [score member ...] zrem key member 同增 zcard key zscore key member zrank key member zrevrank key member

redis 的命令有很多无需把每个命令都背下来只需要牢记5种数据结构的特性再根据实际的需求去寻找需要的命令就OK了。
更多命令见Redis命令参考

客户端通信协议RESP

Redis 定制了RESP协议实现客户端与服务端的正常交互。这是一种基于TCP协议之上简单高效的协议。

客户端请求
如需要在Redis 中存储键值对为hello-world 的数据,需要在客户端发送如下格式的数据(每行使用/r/n进行分割)给Redis服务器。

*3
$3
SET
$5
hello
$5
world

协议说明:
*3表示参数量为3个即本条命令有3个参数
35 $5表示参数的字节数。
上面的数据进行了格式化的显示,实际传输的格式为如下代码

*3/r/n$3/r/nSET/r/n$5/r/nhello/r/n$5/r/nworld/r/n

Redis服务端响应
Redis服务器收到指令正确解析后返回如下数据

+OK

协议说明:
状态回复:+
错误回复:-
整数回复::
字符串回复:$
多条字符串回复:*

Redis客户端

Jedis

jedis实现了RESP协议。

获取jedis

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

Jedis基本使用

// 创建连接
Jedis jedis = new Jedis("localhost", 6379);
// 存储数据
jedis.set("foo", "bar");
// 获取数据
String value = jedis.get("foo");

Jedis连接池的使用

JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost", 6379);
/// Jedis implements Closeable. Hence, the jedis instance will be auto-closed after the last statement.
try (Jedis jedis = pool.getResource()) {
    /// ... do stuff here ... for example
    jedis.set("foo", "bar");
    String foobar = jedis.get("foo");
    jedis.zadd("sose", 0, "car"); jedis.zadd("sose", 0, "bike");
    Set<String> sose = jedis.zrange("sose", 0, -1);
    System.out.print(sose);
}

Spring RedisTemplate

RedisTemplate基本使用

在理解RedisTemplate背后的原理前我们先看看其是如何操作Redis的。

...

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Test
public void test() throws Exception {
    // 保存字符串
    stringRedisTemplate.opsForValue().set("aaa", "111");
    Assert.assertEquals("111", stringRedisTemplate.opsForValue().get("aaa"));
}

...

我们需要向健aaa设置value为111需要先获取ValueOperations对象然后进行相关命令操作。

RedisTemplate源码分析

针对Redis的支持的数据结构,从RedisTemplate源码中可知使用如下类封装了相关数据结构的命令

  • ValueOperations (string)
  • ListOperations (list)
  • SetOperations (set)
  • ZSetOperations (zset)
  • GeoOperations (GEO)
  • HyperLogLogOperations (HyperLogLog)
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {

   ...
   
   private @Nullable ValueOperations<K, V> valueOps;
   private @Nullable ListOperations<K, V> listOps;
   private @Nullable SetOperations<K, V> setOps;
   private @Nullable ZSetOperations<K, V> zSetOps;
   private @Nullable GeoOperations<K, V> geoOps;
   private @Nullable HyperLogLogOperations<K, V> hllOps;
   
   ...
 
 }

在"RedisTemplate基本使用"所示的例子中通过spring的注入注解获取了StringRedisTemplate对象的引用。

@Autowired
private StringRedisTemplate stringRedisTemplate;

StringRedisTemplate又是如何被初始化的呢?
我们找到springboot 源码中的RedisAutoConfiguration如下所示

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

   @Bean
   @ConditionalOnMissingBean(name = "redisTemplate")
   public RedisTemplate<Object, Object> redisTemplate(
         RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
      RedisTemplate<Object, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

   @Bean
   @ConditionalOnMissingBean
   public StringRedisTemplate stringRedisTemplate(
         RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
      StringRedisTemplate template = new StringRedisTemplate();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

}

在RedisAutoConfiguration了初始化了RedisTemplate<Object, Object> 和 StringRedisTemplate对象,他们都依赖的一个参数 redisConnectionFactory,

redisConnectionFactory又是如何创建的呢?

通过IDE(intelliJ IDEA是真好用_)可以看到RedisConnectionFactory有两个实现

redisConnectionFactory实现

查看RedisConnectionFactory、JedisConnectionFactory、LettuceConnectionFactory类可知这边使用抽象工厂模式

基于springboot是插拔式开箱即用特性我猜测这边肯定有地方注入了连接工厂。
在RedisAutoConfiguration类所在的包下找到了JedisConnectionConfiguration、LettuceConnectionConfiguration

@Configuration
@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
class JedisConnectionConfiguration extends RedisConnectionConfiguration {

...

@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
   return createJedisConnectionFactory();
}

...
}
@Configuration
@ConditionalOnClass(RedisClient.class)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {

...
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public LettuceConnectionFactory redisConnectionFactory(
      ClientResources clientResources) throws UnknownHostException {
   LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(
         clientResources, this.properties.getLettuce().getPool());
   return createLettuceConnectionFactory(clientConfig);
}
...

}

根据spring的java 注解自动配置@ConditionalOnClass 发现在最新的springboot2.0中已经移除了jdeis默认集成了Lettuce(另一种redis java客户端的实现)。
所以在自动装配时会使用lettuce作为其连接底层,通过debug发现确实如此

springboot2.0默认装配的Redis连接工厂

再回到StringRedisTemplate的父类RedisTemplate<K, V>这个类,其中K、V是泛型实现参考springboot源码中RedisAutoConfiguration默认自动配置实现,我们可以扩展自己需要的类型如key存储string,对象转成成json string 在redis中存储配置如下代码所示:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisJsonTemplate(RedisConnectionFactory jedisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        @SuppressWarnings("all")
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

ValueOperations、ListOperations等是如何调用jedis或者lettuce 的api的?

跟踪stringRedisTemplate.opsForValue().set("aaa", "111");中的set方法

@Nullable
<T> T execute(RedisCallback<T> callback, boolean b) {
   return template.execute(callback, b);
}

@Override
public void set(K key, V value) {

   byte[] rawValue = rawValue(value);
   execute(new ValueDeserializingRedisCallback(key) {

      @Override
      protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
         connection.set(rawKey, rawValue);
         return null;
      }
   }, true);
}

会统一走execute(命令模式)方法完成操作,注入匿名内部类创建的回调对象用于获取连接后执行具体的指令
查看execute实现可知最终回到RedisTemplate类中的execute执行相关操作。

RedisTemplate类中execute方法如下:

@Nullable
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {

   Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
   Assert.notNull(action, "Callback object must not be null");
   // 获取连接工厂
   RedisConnectionFactory factory = getRequiredConnectionFactory();
   RedisConnection conn = null;
   try {
      // 获取连接  
      if (enableTransactionSupport) {
         // only bind resources in case of potential transaction synchronization
         conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
      } else {
         conn = RedisConnectionUtils.getConnection(factory);
      }

      boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);

      RedisConnection connToUse = preProcessConnection(conn, existingConnection);

      boolean pipelineStatus = connToUse.isPipelined();
      if (pipeline && !pipelineStatus) {
         connToUse.openPipeline();
      }

      RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
      // 使用传递的回调对象执行具体的命令
      T result = action.doInRedis(connToExpose);

      // close pipeline
      if (pipeline && !pipelineStatus) {
         connToUse.closePipeline();
      }

      // TODO: any other connection processing?
      return postProcessResult(result, connToUse, existingConnection);
   } finally {
      // 释放连接
      RedisConnectionUtils.releaseConnection(conn, factory);
   }
}

其流程可以总结为

  1. 获取连接
  2. 执行命令(使用装配的具体Reids客户端完成相关命令)
  3. 释放连接

和前面Jedis连接池的使用流程基本一致。

总结

RedisTemplate对Redis命令进行了统一的封装对外具有一致的api 和配置,内部命令操作具体的实现由注入的redis客户端完成
备注:springboot 1.5 Redis默认使用了jedis 客户端
springboot 2.0 Redis默认使用了lettuce客户端 ,增加了响应式api 的支持,有同学可能对lettuce不了解这里引用一下lettuce项目github 的wiki 来说明一下

Lettuce is a scalable thread-safe Redis client for synchronous, asynchronous and reactive usage. Multiple threads may share one connection if they avoid blocking and transactional operations such as BLPOP and MULTI/EXEC. Lettuce is built with netty. Supports advanced Redis features such as Sentinel, Cluster, Pipelining, Auto-Reconnect and Redis data models.

感觉非常强大的样子😁
最后附上RedisTemplate 操作Redis的demo
https://github.com/yuanzj/SpringBootRedisDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270